package darknet.application.chat.impl;

import darknet.application.chat.impl.net.ChatMessageFactory;
import darknet.application.chat.impl.net.ChatProtocol;
import darknet.application.chat.impl.net.ChatProtocolHandler;
import darknet.application.chat.impl.net.ChatProtocolSender;
import darknet.application.chat.impl.net.response.ListChannelResponse;

import darknet.application.chat.spec.Channel;
import darknet.application.chat.spec.ChatObserver;
import darknet.application.chat.spec.Session;
import darknet.application.chat.spec.ChatApplication;
import darknet.application.chat.spec.MessageObserver;
import darknet.application.chat.impl.net.message.BroadcastChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastExitChannelMessage;
import darknet.application.chat.impl.net.message.BroadcastJoinChannelMessage;
import darknet.application.chat.impl.net.message.ChannelMessage;
import darknet.application.chat.impl.net.message.CloseChannelMessage;
import darknet.application.chat.impl.net.message.ExitChannelMessage;
import darknet.application.chat.impl.net.message.JoinChannelMessage;
import darknet.application.chat.impl.net.message.PrivateMessage;
import darknet.application.chat.impl.net.message.SessionExitMessage;
import darknet.application.chat.impl.net.message.SessionStartMessage;

import darknet.core.spec.net.NetworkManager;
import darknet.core.spec.net.Node;
import darknet.core.spec.net.Protocol;
import darknet.core.spec.net.ProtocolHandler;
import darknet.core.spec.net.ProtocolSender;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Main class for manage chat session.
 * 
 * @author yoyo
 */
public class ChatApplicationImpl implements ChatApplication, MessageObserver {

    // XXX Problems if the nodes create a channel with the same name.
    // Solution : The channel name must have the node hoster.

    /** The network manager */
    private NetworkManager network;

    /** The chat message factory. */
    private ChatMessageFactory factory;

    /** Handler */
    private ChatProtocolHandler handler;

    /** Sender */
    private ChatProtocolSender sender;

    /** Protocol */
    private ChatProtocol protocol;

    /** My node */
    private Node myNode;

    /** All hosted channels */
    private Map<String, ChannelHostedImpl> hostedChannels;

    /** All joined channels. */
    private Map<String, ChannelDistantImpl> joinedChannels;

    /** Set of sessions */
    private Map<String, Session> sessions;
    
    /** Observers */
    private Set<ChatObserver> observers;

    /**
     * Initialise a new instance of a Chat Manager.
     * @param network 
     */
    public ChatApplicationImpl(NetworkManager network) {

        if (network == null) {
            throw new IllegalArgumentException("network is null");
        }
        this.network = network;

        factory = ChatMessageFactory.getInstance(network);

        handler = new ChatProtocolHandler(this, this);
        sender = new ChatProtocolSender();
        protocol = ChatProtocol.getCurrent();

        myNode = network.myNode();
        hostedChannels = new HashMap<String, ChannelHostedImpl>(5);
        sessions = new HashMap<String, Session>(5);

        joinedChannels = new HashMap<String, ChannelDistantImpl>(5);

        observers = new HashSet<ChatObserver>(1);
    }

    @Override
    public Session createSession(String nodeName) throws IOException {

        if (nodeName == null || nodeName.length() == 0) {
            throw new IllegalArgumentException("nodeName is invalid");
        }

        // Session already exist ?
        if (sessions.containsKey(nodeName)) {
            return sessions.get(nodeName);
        }

        // Create and start the session.
        Session session = new SessionImpl(network, network.getNode(nodeName));
        session.start();
        sessions.put(nodeName, session);
        return session;
    }

    @Override
    public Channel createChannel(String channelName) {

        if (channelName == null || channelName.length() == 0) {
            throw new IllegalArgumentException("channelName is invalid");
        }

        // Channel already exist ?
        if (hostedChannels.containsKey(channelName)) {
            return hostedChannels.get(channelName);
        }

        // Create the channel.
        ChannelHostedImpl channel = new ChannelHostedImpl(
                channelName, this, this);
        hostedChannels.put(channelName, channel);

        return channel;
    }

    @Override
    public Set<Channel> getHostedChannels() {
        return new HashSet<Channel> (hostedChannels.values());
    }

    @Override
    public Set<Channel> getChannels() throws IOException {

        Set<Channel> channels = new HashSet<Channel>(30);

        // Get channels for distant nodes.
        for (Node to : getReachableNodes()) {

            ListChannelResponse response =
                    (ListChannelResponse) network.send(
                    factory.makeListChannelMessage(to));

            // Build a distant version of channels
            Set<Channel> distantChannels = response.getChannels();
            for (Channel channel : distantChannels) {

                // build the distant version of a channel
                Channel distantChannel = new ChannelDistantImpl(
                        network, channel.getHoster(), channel.getName(), this);
                channels.add(distantChannel);
            }
        }

        // Add our channels, if there are not in the list
        for (Channel channel : getHostedChannels()) {

            if (!channels.contains(channel)) {
                channels.add(channel);
            }
        }

        return channels;
    }

    @Override
    public Channel getChannel(String channelName) throws IOException {

        Channel channel = null;

        // Parse network channel
        for (Channel current : getChannels()) {

            if (current.getName().equals(channelName)) {
                channel = current;
                break; // TODO Bad loop, but works
            }
        }

        return channel;
    }

    @Override
    public Set<Channel> getJoinedChannels() {
        return new HashSet<Channel>(joinedChannels.values());
    }

    @Override
    public Set<Session> getSessions() {
        return new HashSet<Session>(sessions.values());
    }

    @Override
    public Protocol getProtocol() {
        return protocol;
    }
    
    @Override
    public NetworkManager getNetworkManager() {
        return network;
    }

    @Override
    public ProtocolHandler getHandler() {
        return handler;
    }

    @Override
    public ProtocolSender getSender() {
        return sender;
    }

    @Override
    public Set<Node> getReachableNodes() {

        // XXX Assuming that all nodes of your network have installed the
        // chat plugin
        return new HashSet<Node>(network.getOnlineNodes());
    }

    @Override
    public void addChatObserver(ChatObserver toAdd) {

        if (toAdd == null) {
            throw new IllegalArgumentException("toAdd is null");
        }
        observers.add(toAdd);
    }

    @Override
    public void removeChatObserver(ChatObserver toRemove) {

        if (toRemove == null) {
            throw new IllegalArgumentException("toRemove is null");
        }
        observers.remove(toRemove);
    }

    @Override
    public List<ChatObserver> getChatObservers() {
        
        return new ArrayList<ChatObserver>(observers);
    }

    /**
     * A session start message event is receive, handle it.
     * 
     * @param message
     */
    @Override
    public synchronized void newSessionStartMessage(
            SessionStartMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is null");
        }

        // Session already exist ?
        if (sessions.containsKey(message.getFrom().getName())) {
            return;
        }

        Session session = new SessionImpl(network, message.getFrom());
        sessions.put(message.getFrom().getName(), session);

        // Notify all.
        for (ChatObserver observer : observers) {
            observer.newSessionStart(session);
        }
    }

    @Override
    public synchronized void newPrivateMessage(PrivateMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is null");
        }

        String from = message.getFrom().getName();

        // Session exist ?
        if (!sessions.containsKey(from)) {
            return;
        }

        SessionImpl session = (SessionImpl) sessions.get(from);
        session.newPrivateMessage(message);
    }

    @Override
    public synchronized void newSessionExitMessage(SessionExitMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is null");
        }

        String from = message.getFrom().getName();

        // Session exist ?
        if (!sessions.containsKey(from)) {
            return;
        }

        SessionImpl session = (SessionImpl) sessions.get(from);
        session.newExitSessionMessage(message);

        sessions.remove(from);
    }

    @Override
    public synchronized void newJoinChannelMessage(JoinChannelMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is invalid (null)");
        }

        // A node has joined a hosted channel. Send the event to the channel.
        if (hostedChannels.containsKey(message.getChannelName())) {
            hostedChannels.get(message.getChannelName())
                    .newJoinChannelMessage(message);
        }
    }

     @Override
    public synchronized void newBroadcastJoinChannelMessage(
             BroadcastJoinChannelMessage message) {

         if (message == null) {
             throw new IllegalArgumentException("message is invalid (null)");
         }

         // Do we have joined this channel ?
         if (!joinedChannels.containsKey(message.getChannelName())) {
             return;
         }

         // Send the message to the channel.
         joinedChannels.get(message.getChannelName()).newBroadcastJoinMessage(
                 message);
    }

    @Override
    public synchronized void newChannelMessage(ChannelMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is invalid (null)");
        }

        // Message post in a hosted channel. Send the event to the channel.
        if (hostedChannels.containsKey(message.getChannelName())) {
            hostedChannels.get(message.getChannelName())
                    .newChannelMessage(message);
        }
    }

    @Override
    public synchronized void newExitChannelMessage(ExitChannelMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is invalid (null)");
        }

        // A node has exit our channel. Send the event to the channel.
        if (hostedChannels.containsKey(message.getChannelName())) {
            hostedChannels.get(message.getChannelName())
                    .newExitChannelMessage(message);
        }

        // If the exit message is come from ourself, un-host the channel
        if (message.getFrom().getName().equals(myNode.getName())) {
            hostedChannels.remove(message.getChannelName());
        }
    }

    @Override
    public synchronized void newBroadcastExitChannelMessage(
            BroadcastExitChannelMessage message) {

        if (message == null) {
             throw new IllegalArgumentException("message is invalid (null)");
         }

         // Do we have joined this channel ?
         if (!joinedChannels.containsKey(message.getChannelName())) {
             return;
         }

         // Send the message to the channel.
         joinedChannels.get(message.getChannelName()).newBroadcastExitMessage(
                 message);
    }

    @Override
    public synchronized void newBroadcastChannelMessage(
            BroadcastChannelMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is invalid (null)");
        }
        
        // A message has been post in a joined channel.
        // Sent the event to the channel.
        if (joinedChannels.containsKey(message.getChannelName())) {

            joinedChannels.get(message.getChannelName())
                    .newBroadcastChannelMessage(message);
        }
    }

    @Override
    public synchronized void newCloseChannelMessage(
            CloseChannelMessage message) {

        if (message == null) {
            throw new IllegalArgumentException("message is invalid (null)");
        }

        // A joined channel is closed. Delete it
        if (joinedChannels.containsKey(message.getChannelName())) {
            joinedChannels.get(message.getChannelName()).newCloseChannelMessage(
                    message);
            joinedChannels.remove(message.getChannelName());
        }
    }

   

    
    
    void addJoinedChannel(ChannelDistantImpl channel) {

        if (channel == null) {
            throw new IllegalArgumentException("channel is invalid (null)");
        }

        joinedChannels.put(channel.getName(), channel);
    }

    void closeHostedChannel(ChannelHostedImpl channel) {

        if (channel == null) {
            throw new IllegalArgumentException("channel is invalid (null)");
        }

        hostedChannels.remove(channel.getName());
    }


}
